// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
//
// Copyright (C) 2003-2004 Nominum, Inc.
// 
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
// OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

package org.xbill.DNS;

import java.io.*;
import java.net.*;

import org.xbill.DNS.utils.*;

/**
 * Tokenizer is used to parse DNS records and zones from text format,
 * 
 * @author Brian Wellington
 * @author Bob Halley
 */

public class Tokenizer {

	private static String delim = " \t\n;()\"";
	private static String quotes = "\"";

	/** End of file */
	public static final int EOF = 0;

	/** End of line */
	public static final int EOL = 1;

	/** Whitespace; only returned when wantWhitespace is set */
	public static final int WHITESPACE = 2;

	/** An identifier (unquoted string) */
	public static final int IDENTIFIER = 3;

	/** A quoted string */
	public static final int QUOTED_STRING = 4;

	/** A comment; only returned when wantComment is set */
	public static final int COMMENT = 5;

	private PushbackInputStream is;
	private boolean ungottenToken;
	private int multiline;
	private boolean quoting;
	private String delimiters;
	private Token current;
	private StringBuffer sb;
	private boolean wantClose;

	private String filename;
	private int line;

	public static class Token {
		/** The type of token. */
		public int type;

		/** The value of the token, or null for tokens without values. */
		public String value;

		private Token() {
			type = -1;
			value = null;
		}

		private Token set(int type, StringBuffer value) {
			if (type < 0)
				throw new IllegalArgumentException();
			this.type = type;
			this.value = value == null ? null : value.toString();
			return this;
		}

		/**
		 * Converts the token to a string containing a representation useful for
		 * debugging.
		 */
		public String toString() {
			switch (type) {
			case EOF:
				return "<eof>";
			case EOL:
				return "<eol>";
			case WHITESPACE:
				return "<whitespace>";
			case IDENTIFIER:
				return "<identifier: " + value + ">";
			case QUOTED_STRING:
				return "<quoted_string: " + value + ">";
			case COMMENT:
				return "<comment: " + value + ">";
			default:
				return "<unknown>";
			}
		}

		/** Indicates whether this token contains a string. */
		public boolean isString() {
			return (type == IDENTIFIER || type == QUOTED_STRING);
		}

		/** Indicates whether this token contains an EOL or EOF. */
		public boolean isEOL() {
			return (type == EOL || type == EOF);
		}
	}

	static class TokenizerException extends TextParseException {
		String message;

		public TokenizerException(String filename, int line, String message) {
			super(filename + ":" + line + ": " + message);
			this.message = message;
		}

		public String getBaseMessage() {
			return message;
		}
	}

	/**
	 * Creates a Tokenizer from an arbitrary input stream.
	 * 
	 * @param is
	 *            The InputStream to tokenize.
	 */
	public Tokenizer(InputStream is) {
		if (!(is instanceof BufferedInputStream))
			is = new BufferedInputStream(is);
		this.is = new PushbackInputStream(is, 2);
		ungottenToken = false;
		multiline = 0;
		quoting = false;
		delimiters = delim;
		current = new Token();
		sb = new StringBuffer();
		filename = "<none>";
		line = 1;
	}

	/**
	 * Creates a Tokenizer from a string.
	 * 
	 * @param s
	 *            The String to tokenize.
	 */
	public Tokenizer(String s) {
		this(new ByteArrayInputStream(s.getBytes()));
	}

	/**
	 * Creates a Tokenizer from a file.
	 * 
	 * @param f
	 *            The File to tokenize.
	 */
	public Tokenizer(File f) throws FileNotFoundException {
		this(new FileInputStream(f));
		wantClose = true;
		filename = f.getName();
	}

	private int getChar() throws IOException {
		int c = is.read();
		if (c == '\r') {
			int next = is.read();
			if (next != '\n')
				is.unread(next);
			c = '\n';
		}
		if (c == '\n')
			line++;
		return c;
	}

	private void ungetChar(int c) throws IOException {
		if (c == -1)
			return;
		is.unread(c);
		if (c == '\n')
			line--;
	}

	private int skipWhitespace() throws IOException {
		int skipped = 0;
		while (true) {
			int c = getChar();
			if (c != ' ' && c != '\t') {
				if (!(c == '\n' && multiline > 0)) {
					ungetChar(c);
					return skipped;
				}
			}
			skipped++;
		}
	}

	private void checkUnbalancedParens() throws TextParseException {
		if (multiline > 0)
			throw exception("unbalanced parentheses");
	}

	/**
	 * Gets the next token from a tokenizer.
	 * 
	 * @param wantWhitespace
	 *            If true, leading whitespace will be returned as a token.
	 * @param wantComment
	 *            If true, comments are returned as tokens.
	 * @return The next token in the stream.
	 * @throws TextParseException
	 *             The input was invalid.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public Token get(boolean wantWhitespace, boolean wantComment)
			throws IOException {
		int type;
		int c;

		if (ungottenToken) {
			ungottenToken = false;
			if (current.type == WHITESPACE) {
				if (wantWhitespace)
					return current;
			} else if (current.type == COMMENT) {
				if (wantComment)
					return current;
			} else {
				if (current.type == EOL)
					line++;
				return current;
			}
		}
		int skipped = skipWhitespace();
		if (skipped > 0 && wantWhitespace)
			return current.set(WHITESPACE, null);
		type = IDENTIFIER;
		sb.setLength(0);
		while (true) {
			c = getChar();
			if (c == -1 || delimiters.indexOf(c) != -1) {
				if (c == -1) {
					if (quoting)
						throw exception("EOF in " + "quoted string");
					else if (sb.length() == 0)
						return current.set(EOF, null);
					else
						return current.set(type, sb);
				}
				if (sb.length() == 0 && type != QUOTED_STRING) {
					if (c == '(') {
						multiline++;
						skipWhitespace();
						continue;
					} else if (c == ')') {
						if (multiline <= 0)
							throw exception("invalid " + "close "
									+ "parenthesis");
						multiline--;
						skipWhitespace();
						continue;
					} else if (c == '"') {
						if (!quoting) {
							quoting = true;
							delimiters = quotes;
							type = QUOTED_STRING;
						} else {
							quoting = false;
							delimiters = delim;
							skipWhitespace();
						}
						continue;
					} else if (c == '\n') {
						return current.set(EOL, null);
					} else if (c == ';') {
						while (true) {
							c = getChar();
							if (c == '\n' || c == -1)
								break;
							sb.append((char) c);
						}
						if (wantComment) {
							ungetChar(c);
							return current.set(COMMENT, sb);
						} else if (c == -1 && type != QUOTED_STRING) {
							checkUnbalancedParens();
							return current.set(EOF, null);
						} else if (multiline > 0) {
							skipWhitespace();
							sb.setLength(0);
							continue;
						} else
							return current.set(EOL, null);
					} else
						throw new IllegalStateException();
				} else
					ungetChar(c);
				break;
			} else if (c == '\\') {
				c = getChar();
				if (c == -1)
					throw exception("unterminated escape sequence");
				sb.append('\\');
			} else if (quoting && c == '\n') {
				throw exception("newline in quoted string");
			}
			sb.append((char) c);
		}
		if (sb.length() == 0 && type != QUOTED_STRING) {
			checkUnbalancedParens();
			return current.set(EOF, null);
		}
		return current.set(type, sb);
	}

	/**
	 * Gets the next token from a tokenizer, ignoring whitespace and comments.
	 * 
	 * @return The next token in the stream.
	 * @throws TextParseException
	 *             The input was invalid.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public Token get() throws IOException {
		return get(false, false);
	}

	/**
	 * Returns a token to the stream, so that it will be returned by the next
	 * call to get().
	 * 
	 * @throws IllegalStateException
	 *             There are already ungotten tokens.
	 */
	public void unget() {
		if (ungottenToken)
			throw new IllegalStateException("Cannot unget multiple tokens");
		if (current.type == EOL)
			line--;
		ungottenToken = true;
	}

	/**
	 * Gets the next token from a tokenizer and converts it to a string.
	 * 
	 * @return The next token in the stream, as a string.
	 * @throws TextParseException
	 *             The input was invalid or not a string.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public String getString() throws IOException {
		Token next = get();
		if (!next.isString()) {
			throw exception("expected a string");
		}
		return next.value;
	}

	private String _getIdentifier(String expected) throws IOException {
		Token next = get();
		if (next.type != IDENTIFIER)
			throw exception("expected " + expected);
		return next.value;
	}

	/**
	 * Gets the next token from a tokenizer, ensures it is an unquoted string,
	 * and converts it to a string.
	 * 
	 * @return The next token in the stream, as a string.
	 * @throws TextParseException
	 *             The input was invalid or not an unquoted string.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public String getIdentifier() throws IOException {
		return _getIdentifier("an identifier");
	}

	/**
	 * Gets the next token from a tokenizer and converts it to a long.
	 * 
	 * @return The next token in the stream, as a long.
	 * @throws TextParseException
	 *             The input was invalid or not a long.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public long getLong() throws IOException {
		String next = _getIdentifier("an integer");
		if (!Character.isDigit(next.charAt(0)))
			throw exception("expected an integer");
		try {
			return Long.parseLong(next);
		} catch (NumberFormatException e) {
			throw exception("expected an integer");
		}
	}

	/**
	 * Gets the next token from a tokenizer and converts it to an unsigned 32
	 * bit integer.
	 * 
	 * @return The next token in the stream, as an unsigned 32 bit integer.
	 * @throws TextParseException
	 *             The input was invalid or not an unsigned 32 bit integer.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public long getUInt32() throws IOException {
		long l = getLong();
		if (l < 0 || l > 0xFFFFFFFFL)
			throw exception("expected an 32 bit unsigned integer");
		return l;
	}

	/**
	 * Gets the next token from a tokenizer and converts it to an unsigned 16
	 * bit integer.
	 * 
	 * @return The next token in the stream, as an unsigned 16 bit integer.
	 * @throws TextParseException
	 *             The input was invalid or not an unsigned 16 bit integer.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public int getUInt16() throws IOException {
		long l = getLong();
		if (l < 0 || l > 0xFFFFL)
			throw exception("expected an 16 bit unsigned integer");
		return (int) l;
	}

	/**
	 * Gets the next token from a tokenizer and converts it to an unsigned 8 bit
	 * integer.
	 * 
	 * @return The next token in the stream, as an unsigned 8 bit integer.
	 * @throws TextParseException
	 *             The input was invalid or not an unsigned 8 bit integer.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public int getUInt8() throws IOException {
		long l = getLong();
		if (l < 0 || l > 0xFFL)
			throw exception("expected an 8 bit unsigned integer");
		return (int) l;
	}

	/**
	 * Gets the next token from a tokenizer and parses it as a TTL.
	 * 
	 * @return The next token in the stream, as an unsigned 32 bit integer.
	 * @throws TextParseException
	 *             The input was not valid.
	 * @throws IOException
	 *             An I/O error occurred.
	 * @see TTL
	 */
	public long getTTL() throws IOException {
		String next = _getIdentifier("a TTL value");
		try {
			return TTL.parseTTL(next);
		} catch (NumberFormatException e) {
			throw exception("expected a TTL value");
		}
	}

	/**
	 * Gets the next token from a tokenizer and parses it as if it were a TTL.
	 * 
	 * @return The next token in the stream, as an unsigned 32 bit integer.
	 * @throws TextParseException
	 *             The input was not valid.
	 * @throws IOException
	 *             An I/O error occurred.
	 * @see TTL
	 */
	public long getTTLLike() throws IOException {
		String next = _getIdentifier("a TTL-like value");
		try {
			return TTL.parse(next, false);
		} catch (NumberFormatException e) {
			throw exception("expected a TTL-like value");
		}
	}

	/**
	 * Gets the next token from a tokenizer and converts it to a name.
	 * 
	 * @param origin
	 *            The origin to append to relative names.
	 * @return The next token in the stream, as a name.
	 * @throws TextParseException
	 *             The input was invalid or not a valid name.
	 * @throws IOException
	 *             An I/O error occurred.
	 * @throws RelativeNameException
	 *             The parsed name was relative, even with the origin.
	 * @see Name
	 */
	public Name getName(Name origin) throws IOException {
		String next = _getIdentifier("a name");
		try {
			Name name = Name.fromString(next, origin);
			if (!name.isAbsolute())
				throw new RelativeNameException(name);
			return name;
		} catch (TextParseException e) {
			throw exception(e.getMessage());
		}
	}

	/**
	 * Gets the next token from a tokenizer and converts it to an IP Address.
	 * 
	 * @param family
	 *            The address family.
	 * @return The next token in the stream, as an InetAddress
	 * @throws TextParseException
	 *             The input was invalid or not a valid address.
	 * @throws IOException
	 *             An I/O error occurred.
	 * @see Address
	 */
	public InetAddress getAddress(int family) throws IOException {
		String next = _getIdentifier("an address");
		try {
			return Address.getByAddress(next, family);
		} catch (UnknownHostException e) {
			throw exception(e.getMessage());
		}
	}

	/**
	 * Gets the next token from a tokenizer, which must be an EOL or EOF.
	 * 
	 * @throws TextParseException
	 *             The input was invalid or not an EOL or EOF token.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public void getEOL() throws IOException {
		Token next = get();
		if (next.type != EOL && next.type != EOF) {
			throw exception("expected EOL or EOF");
		}
	}

	/**
	 * Returns a concatenation of the remaining strings from a Tokenizer.
	 */
	private String remainingStrings() throws IOException {
		StringBuffer buffer = null;
		while (true) {
			Tokenizer.Token t = get();
			if (!t.isString())
				break;
			if (buffer == null)
				buffer = new StringBuffer();
			buffer.append(t.value);
		}
		unget();
		if (buffer == null)
			return null;
		return buffer.toString();
	}

	/**
	 * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
	 * them together, and converts the base64 encoded data to a byte array.
	 * 
	 * @param required
	 *            If true, an exception will be thrown if no strings remain;
	 *            otherwise null be be returned.
	 * @return The byte array containing the decoded strings, or null if there
	 *         were no strings to decode.
	 * @throws TextParseException
	 *             The input was invalid.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public byte[] getBase64(boolean required) throws IOException {
		String s = remainingStrings();
		if (s == null) {
			if (required)
				throw exception("expected base64 encoded string");
			else
				return null;
		}
		byte[] array = base64.fromString(s);
		if (array == null)
			throw exception("invalid base64 encoding");
		return array;
	}

	/**
	 * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
	 * them together, and converts the base64 encoded data to a byte array.
	 * 
	 * @return The byte array containing the decoded strings, or null if there
	 *         were no strings to decode.
	 * @throws TextParseException
	 *             The input was invalid.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public byte[] getBase64() throws IOException {
		return getBase64(false);
	}

	/**
	 * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
	 * them together, and converts the hex encoded data to a byte array.
	 * 
	 * @param required
	 *            If true, an exception will be thrown if no strings remain;
	 *            otherwise null be be returned.
	 * @return The byte array containing the decoded strings, or null if there
	 *         were no strings to decode.
	 * @throws TextParseException
	 *             The input was invalid.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public byte[] getHex(boolean required) throws IOException {
		String s = remainingStrings();
		if (s == null) {
			if (required)
				throw exception("expected hex encoded string");
			else
				return null;
		}
		byte[] array = base16.fromString(s);
		if (array == null)
			throw exception("invalid hex encoding");
		return array;
	}

	/**
	 * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
	 * them together, and converts the hex encoded data to a byte array.
	 * 
	 * @return The byte array containing the decoded strings, or null if there
	 *         were no strings to decode.
	 * @throws TextParseException
	 *             The input was invalid.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public byte[] getHex() throws IOException {
		return getHex(false);
	}

	/**
	 * Gets the next token from a tokenizer and decodes it as hex.
	 * 
	 * @return The byte array containing the decoded string.
	 * @throws TextParseException
	 *             The input was invalid.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public byte[] getHexString() throws IOException {
		String next = _getIdentifier("a hex string");
		byte[] array = base16.fromString(next);
		if (array == null)
			throw exception("invalid hex encoding");
		return array;
	}

	/**
	 * Gets the next token from a tokenizer and decodes it as base32.
	 * 
	 * @param b32
	 *            The base32 context to decode with.
	 * @return The byte array containing the decoded string.
	 * @throws TextParseException
	 *             The input was invalid.
	 * @throws IOException
	 *             An I/O error occurred.
	 */
	public byte[] getBase32String(base32 b32) throws IOException {
		String next = _getIdentifier("a base32 string");
		byte[] array = b32.fromString(next);
		if (array == null)
			throw exception("invalid base32 encoding");
		return array;
	}

	/**
	 * Creates an exception which includes the current state in the error
	 * message
	 * 
	 * @param s
	 *            The error message to include.
	 * @return The exception to be thrown
	 */
	public TextParseException exception(String s) {
		return new TokenizerException(filename, line, s);
	}

	/**
	 * Closes any files opened by this tokenizer.
	 */
	public void close() {
		if (wantClose) {
			try {
				is.close();
			} catch (IOException e) {
			}
		}
	}

	protected void finalize() {
		close();
	}

}
